查看原文
其他

2015年AliCrackMe学习Android逆向

直木 看雪学苑 2022-07-01
本文为看雪论坛优秀文章
看雪论坛作者ID:直木

总感觉以前学的 Android 逆向不扎实,现在通过这五道题复习一下。


1


AliCrackme_1


1、运行题目:MuMu模拟器

需要输入正确的密码:

 

2、反编译分析

首先对apk文件进行反编译,找到登录按钮所在Activity(MainActivity)的代码并分析:

table 是使用方法 getTableFromPic 得到的一张字符表;

pw 是使用方法 getPwdFromPic 通过 table 进行编码后的正确密码;
enPassword 是对在输入框中输入的字符串进行编码的结果。


3、泄漏 table 和 pw

方法一:ddms/adb logcat:

它们都会通过Log打印出来,如上图中红框所示。所以,我们可以利用 DDMS 或者 adb logcat 查看log 。通过在APP中随便输入字符串,点击“登录”按钮,就能从Logcat中查看到打印值。如下图所示,输入“123456”。

 
后面写一个脚本,使用MainActivity中本身提供的解码方法,传入参数 table 和 pw ,就能得到正确密码。

方法二:逆向分析

如果没有Log.d方法,该怎么办呢?前面已经分析出 table 和 pw 分别是通过方法 getTableFromPic 和getPwdFromPic得到的,那么就分析这两个方法。

它们都是通过对assets目录下的logo.png图片进行计算得到返回值。那么就可以创建一个Android项目,把logo.png拷贝到新项目的assets目录下。然后将getTableFromPic 和getPwdFromPic拷贝到AndroidTest目录下的ExampleInstrumentedTest文件中。

package com.lzx.ufo;
import android.content.Context;import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;import org.junit.runner.RunWith;
import java.io.IOException;import java.io.InputStream;
import static org.junit.Assert.*;
/** * Instrumented test, which will execute on an Android device. * * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */@RunWith(AndroidJUnit4.class)public class ExampleInstrumentedTest { @Test public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.lzx.ufo", appContext.getPackageName());
Log.d("UFO======",getTableFromPic(appContext)); Log.d("UFO======",getPwdFromPic(appContext)); }
protected String getTableFromPic(Context appContext) { InputStream is = null; String value = ""; try { is = appContext.getResources().getAssets().open("logo.png"); int lenght = is.available();// Log.d("UFO","Table-lenght:"+lenght);
byte[] b = new byte[lenght]; is.read(b, 0, lenght); byte[] data = new byte[768]; System.arraycopy(b, 89473, data, 0, 768); String value2 = new String(data, "utf-8"); if (is == null) { return value2; } try { is.close(); return value2; } catch (IOException e) { return value2; } } catch (Exception e2) { e2.printStackTrace(); if (is == null) { return value; } try { is.close(); return value; } catch (IOException e3) { return value; } } catch (Throwable th) { if (is != null) { try { is.close();
} catch (IOException e4) {
} } } return value; }
protected String getPwdFromPic(Context appContext) {
InputStream is = null; String value = ""; try { is = appContext.getResources().getAssets().open("logo.png"); int lenght = is.available();
// Log.d("UFO","Pwd-lenght:"+lenght);
byte[] b = new byte[lenght]; is.read(b, 0, lenght); byte[] data = new byte[18]; System.arraycopy(b, 91265, data, 0, 18); String value2 = new String(data, "utf-8"); if (is == null) { return value2; } try { is.close(); return value2; } catch (IOException e) { return value2; } } catch (Exception e2) { e2.printStackTrace(); if (is == null) { return value; } try { is.close(); return value; } catch (IOException e3) { return value; } } catch (Throwable th) { if (is != null) { try { is.close(); } catch (IOException e4) { } } } return value; }}


4、编写脚本计算flag

方法一:拷贝题目中的解码方法,使用Java代码解题

public class Answer{ public static void main(String[] args){
String table = "一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐"; String pwd = "义弓么丸广之"; System.out.println(aliCodeToBytes(table,pwd)); } private static String aliCodeToBytes(String codeTable, String strCmd) { StringBuilder sb = new StringBuilder(); byte[] cmdBuffer = new byte[strCmd.length()]; for (int i = 0; i < strCmd.length(); i++) { cmdBuffer[i] = (byte) codeTable.indexOf(strCmd.charAt(i)); sb.append((char)cmdBuffer[i]); } return sb.toString(); }}

方法二:根据解码方法,编写对应的Python脚本

table = [ '一', '乙', '二', '十', '丁', '厂', '七', '卜', '人', '入', '八', '九', '几', '儿', '了', '力', '乃', '刀', '又', '三', '于', '干', '亏', '士', '工', '土', '才', '寸', '下', '大', '丈', '与', '万', '上', '小', '口', '巾', '山', '千', '乞', '川', '亿', '个', '勺', '久', '凡', '及', '夕', '丸', '么', '广', '亡', '门', '义', '之', '尸', '弓', '己', '已', '子', '卫', '也', '女', '飞', '刃', '习', '叉', '马', '乡', '丰', '王', '井', '开', '夫', '天', '无', '元', '专', '云', '扎', '艺', '木', '五', '支', '厅', '不', '太', '犬', '区', '历', '尤', '友', '匹', '车', '巨', '牙', '屯', '比', '互', '切', '瓦', '止', '少', '日', '中', '冈', '贝', '内', '水', '见', '午', '牛', '手', '毛', '气', '升', '长', '仁', '什', '片', '仆', '化', '仇', '币', '仍', '仅', '斤', '爪', '反', '介', '父', '从', '今', '凶', '分', '乏', '公', '仓', '月', '氏', '勿', '欠', '风', '丹', '匀', '乌', '凤', '勾', '文', '六', '方', '火', '为', '斗', '忆', '订', '计', '户', '认', '心', '尺', '引', '丑', '巴', '孔', '队', '办', '以', '允', '予', '劝', '双', '书', '幻', '玉', '刊', '示', '末', '未', '击', '打', '巧', '正', '扑', '扒', '功', '扔', '去', '甘', '世', '古', '节', '本', '术', '可', '丙', '左', '厉', '右', '石', '布', '龙', '平', '灭', '轧', '东', '卡', '北', '占', '业', '旧', '帅', '归', '且', '旦', '目', '叶', '甲', '申', '叮', '电', '号', '田', '由', '史', '只', '央', '兄', '叼', '叫', '另', '叨', '叹', '四', '生', '失', '禾', '丘', '付', '仗', '代', '仙', '们', '仪', '白', '仔', '他', '斥', '瓜', '乎', '丛', '令', '用', '甩', '印', '乐']
pw= ['义','弓','么','丸','广','之']
flag = ''pw_len = 0
print(len(table))print(len(pw))
for i in range(len(pw)): for j in range(len(table)): if table[j] == pw[i]: flag += chr(j) break
print(flag)




2


AliCrackme_2


1、运行题目:Nexus 5 , Android 4.4.4



2、 反编译分析

jadx反编译,发现校验方法 securityCheck 是一个native方法。


于是,使用ida打开文件 libcrackme.so ,找到securityCheck对应的JNI函数 Java_com_yaotong_crackme_MainActivity_securityCheck 。修改一些变量名和变量类型(a1的类型修改为JNIEnv*)。


从返回值往前分析,我们需要返回true,也就是1,那就需要输入的字符串和off_628C存储的字符串相等。
 

将 wojiushidaan 输入,显示输入密码错误。因此,可以猜测某个地方在执行while循环之前修改了变量 aWojiushidaan 的值。
 
到这,目的就很明确了,找到变量aWojiushidaan 变换后的值。

3、 泄漏aWojiushidaan变换后的值,得到flag

方法一:frida hook
// readflag.jsfunction hook_lib(){ var so_name = "libcrackme.so"; // lib名称 var flag_offset = 0x00004450; // flag,也就是aWojiushidaan变量的地址 var so_base = Module.findBaseAddress(so_name); // lib基地址 var security_check = Module.findExportByName(so_name,"Java_com_yaotong_crackme_MainActivity_securityCheck"); // jni函数地址 var flag_addr = parseInt(so_base, 16)+flag_offset; // 计算flag的真实地址 var flag_ptr = new NativePointer(flag_addr); // 转换为nativepointer console.log("libcrackme.so base addr: ",so_base); console.log("flag addr: ", flag_addr); console.log("function security_check addr: ",security_check);
Interceptor.attach(security_check,{ onEnter: function(args){ // jni函数进入时 console.log("---- enter ----"); var flag0 = flag_ptr.readByteArray(0x20); // 打印内存中的值 console.log(hexdump(flag0,{ offset: 0, length: 0x20, header: true, ansi: false })) console.log("---- enter end ----"); }, onLeave: function(retval){ // jni函数返回时 var flag = flag_ptr.readByteArray(0x20); //打印内存中的值
console.log("---- leave ----"); console.log(hexdump(flag,{ offset: 0, length: 0x20, header: true, ansi: false })); console.log("---- leave end ----"); } });}
function main(){ hook_lib();}
setImmediate(main);
// frida -R -l readflag.js com.yaotong.crackme


得到结果:aiyou,bucuoo。

PS: 通过结果可以看出:变量aWojiushidaan 在进入函数 Java_com_yaotong_crackme_MainActivity_securityCheck 之前就已经被改变了。

和参考文献中https://www.secpulse.com/archives/5731.html中所说的是函数 Java_com_yaotong_crackme_MainActivity_securityCheck中的前面部分改变了变量 aWojiushidaan 有出入。而在后面的动态调试过程中可以看到,aWojiushidaan的值确实在执行函数Java_com_yaotong_crackme_MainActivity_securityCheck 之前就已经被改变。

方法二:patch so文件

函数Java_com_yaotong_crackme_MainActivity_securityCheck中,比较之前有一个 _android_log_print 函数。可以修改打印的内容,直接利用这个函数输出此时 aWojiushidaan 的值。如下图所示,当点击按钮时,会输出:I yaotong : SecurityCheck Started...


查看汇编代码,如下图所示。在0x12A4地址处正好将 aWojiushidaan 的值赋给寄存器R2,所以将0x1284~0x129C的内容(也就是调用GetStringUTFChars部分)修改为NOP。

然后因为R1是第二个参数,为了在输出的时候Log的TAG不变,这里将0x12A0和0x12A4中的R1修改为R3。


下面开始patch:
 
1、使用apktool反编译


2、在IDA中F2修改,确认修改正确后,使用010Editor进行patch。
 

3、回编译,签名。
apktool b AliCrackme_2_patch # 回编译cd AliCrackme_2_patch/dist/keytool -genkey -alias test1.keystore -keyalg RSA -validity 1000000 -keystore test.keystore # 生成keystore文件jarsigner -verbose -keystore test.keystore -signedjar AliCrackme_2_signed.apk AliCrackme_2.apk test1.keystore # 签名

4、卸载原来的app,重新安装,运行,查看日志。
adb install AliCrackme_2_signed.apkadb shell ps | grep com.yaotong.crackmeadb logcat --pid=xxx


方法三:frida hook2


做完之后,搜了一下这一题的答案,发现不用这么复杂。
 
https://blog.csdn.net/weixin_42011443/article/details/105897429
function hook(){
Java.perform(function(){ var so_name = "libcrackme.so"; // lib名称 var flag_offset = 0x0000628C; // off_628C的地址 var so_base = Module.findBaseAddress(so_name); // lib基地址 var flag = Memory.readUtf8String(Memory.readPointer(so_base.add(flag_offset))); console.log(flag); });}
function main(){ hook();}
setImmediate(main);


方法四:动态调试(过反调试)

1、首先从AndroidManifest.xml中发现没有将android:debuggable属性设置为true。

这里不再用apktool等命令行工具反编译、回编译和签名了。用AndroidKiller工具来操作:a)添加android:debuggable属性,并设置为true;2)菜单->Android->编译。

生成的新apk如下:

 

2、安装新apk。
adb install AliCrackme_2_killer.apkbr

3、push android_server,修改权限,运行。
adb push android_server /data/local/tmp/adb shellsu # 注意需要切换root用户,不然后面attach process的时候只会有两个进程,会找不到目标进程cd /data/local/tmp/chmod 777 android_server./android_server

4、另开一个终端,转发端口。
adb forward tcp:23946 tcp:23946

5、用IDA打开libcrackme.so,设置调试选项:Debugger -> select debugger。

Debugger->Debugger options... 勾选三个。


Suspend on process entry point :在进程入口点挂起

Suspend on thread start/exit:在线程开始/退出时挂起

Suspend on library load/unload:在库加载/卸载时挂起

Debugger->Process options...

 
6、在手机上启动app,然后在IDA中attach进程。
IDA:Debugger->Attach to process ... ,选择要调试的进程名,确认。会弹出一个对话框,让你确认so文件是否一样,选择“same”。


进入调试界面后,它会断在libc.so中。attach之后,总是会停在这里的,因为libc.so 是 native层中最基本的函数库,所有上层的调用都会经过libc。
 
 
7、找到函数Java_com_yaotong_crackme_MainActivity_securityCheck。找这个函数的方法。
 
在Module list窗口(Debugger->Debugger windows->Module list)中找到libcrackme.so,双击它。


然后在打开的新窗口里面找到这个函数,双击它。


双击之后进入函数实现。
 

找到函数Java_com_yaotong_crackme_MainActivity_securityCheck地址的另一个方法是另开一个ida实例,找到该函数的偏移地址,然后通过ctrl + s找到libcrackme.so的基址,两者相加就得到了它的地址。但是不知道为什么,我这里ctrl + s 找不到该so文件的基址。
 

8、找到while循环要比较的地方,发现flag。

下图红框框中的指令对应while循环中第一行代码,取v6中的内容,也就是flag。


虽然这里已经拿到flag了,但是这一题有反调试,为了学习的目的,还是继续看看怎么过反调试。

过反调试


1、查看反调试效果。

在上图红框地方(0x750982A8 LDRB R3, [R2])下断点,按三下F9按钮,程序直接退出了,所以这里是做了反调试检测。


2、so文件加载的过程如下所示。

在加载so文件时,.init和.init_array两个section会做初始化工作:先执行.init段中的代码,然后顺序执行.init_array中的函数。

.init_array是函数指针数组,会按照在源码中声明的顺序将各个相应的函数填到该数组中。接着,JNI_OnLoad在so被 System.loadLibrary 调用的时候执行,它的执行时刻晚于.init_array,早于native方法的执行。
.init -> .init_array -> JNI_Onload -> java_com_xxx

先在JNI_Onload下断点,看看反调试检测是不是在这里做的。调试的前面几步准备步骤和之前一样:
 
a.root权限执行android_server
 
b.端口转发:adb forward tcp:23946 tcp:23946
 
c.ida中和前面一样设置调试选项,注意Debugger options中三个选项要勾上
 
3、接下来就不同了,因为要在JNI_OnLoad下断点,而被调试程序如果一运行就会执行 static 中的语句,所以需要以调试模式启动app,让程序停在加载so文件之前。 
adb shell am start -D -n com.yaotong.crackme/.MainActivity

4、attach进程,在module list窗口搜索libcrackme.so,发现没有,说明程序确实停在加载该so文件之前。


5、连接jdb,然后 F9 运行。
adb shell ps | grep com.yaotong.crackme # 查看pidadb forward tcp:8700 jdwp:4134 # 端口转发,jdwp:上一步找到的pidjdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 # jdb连接


运行程序,连接成功的同时,IDA会成功加载程序,PC跳转到linker模块:


6、同样,通过module list窗口,找到so文件,然后找到JNI_Onload函数,下断点,然后F9运行到这里。


7、F8 单步步出 调试,发现每次在执行到BLX R7的时候,程序就会退出,所以反调试检测应该在这。查看此时的R7寄存器,它的值是pthread_create函数,所以是创建了一个新线程来进行检测。


点击这条指令,tab键(F5也可以,我更喜欢tab)切换到C伪代码。所以,off_754512B4是pthread_create函数,那么第三个参数sub_7544C6A4就是线程执行的函数,反调试基本就在这里面了。继续分析下去。
 
双击off_754512B4,发现它是个函数指针?没有啥想法,先放放,继续往下走。


这里因为经过了动态调试,所以函数名中sub后面接的地址很大,如果是另开一个ida实例进行静态分析,sub后面接的就是该函数在so文件中的偏移地址,比如这里sub_7544C6A4就会是sub_16A4。


8、结束调试,开始静态分析。进入函数sub_7544C6A4,是个死循环,里面执行两个函数。


双击进入sub_7544C30C,我懵了,除了_aeabi_memset 其他都不认识。然后双击进入函数off_754512B0,又是个函数指针。我知道反调试应该就在sub_7544C6A4,但是我看不懂。

回到JNI_Onload函数中,再看看有没有别的相关函数,下一个执行的函数是sub_7544C7F4,双击进去,还是看不懂,但是发现很多jolin函数。这时候我知道靠我自己这个菜鸡进行静态分析是不行了,那就试试动态调试吧。
 
9、动态调试。从BLX R7 F7进去,一步一步调试,看看到底是哪里退出程序的。经过两次调试,发现在函数sub_7544C30C执行完loc_75245600这个块之后就会退出程序,可以看到这个块最后是跳转到kill函数去执行。
 

对应伪代码如下图所示,所以,如果我们将BGE loc_75245600改成BLT loc_75245600 或者 nop就可以改变流程,不执行kill函数,而是直接返回。



选中该指令,切换到Hex View,F2修改,将指令改成nop( 00 00 A0 E1),然后F2保存。同时,另开一个IDA实例,找到该指令的偏移0x15D8(因为原来那个经过动态调试,指令地址已经不是偏移了)。

用010Editor修改,然后回编译、签名、重新安装,在函数Java_com_yaotong_crackme_MainActivity_securityCheck里循环中第一条指令LDRB R3, [R2]处下断点,attach进程之后,点击“输入密码”按钮,然后F9运行到断点,如下图所示,说明成功绕过反调试,并可以看到此时变量aWojiushidaan的值为flag。


10、静态分析。找到一篇大佬写的 2015年AliCrackMe第二题的分析之人肉过反调试 ,分析得特别好。
 
原来题目在.init_array段中通过dlsym获取动态库的函数指针,将所有用到的函数都用函数指针保存起来,隐藏了函数本身的样式。

解密出这些函数之后,可以在sub_7544C30C函数中发现getpid、kill,结合pthread_cretae函数,可以推断出是通过检测TracerPid来进行反调试。值得一提的是在函数中kill的实现是这样的:向对应pid发送9(SIGKILL)信号,以干掉进程。
(*(void (__fastcall **)(int, signed int))((char *)&GLOBAL_OFFSET_TABLE_ + (_DWORD)v3 + (unsigned int)&dword_1C))( v0, 9);

jolin函数的作用就是修改了真正的密码,将wojiushidaan改成了flag。但是它被加密了,而JNI_Onload中pthread_create函数后面的sub_7544C7F4的作用就是解密执行它。
 
简单说一下这种反调试检测的原理:当一个进程没有被调试时,它对应/proc/<pid>/status文件中TracerPid字段的值是0;当该进程被调试时,TracerPid会被写入调试进程的pid,当然这里就是android_server的pid了。
 
对安卓反调试和校验检测的一些实践与结论 总结了很多反调试手段。
 
现在程序反调试的过程很清晰了,就是在sub_7544C6A4里面,只要不执行它,也能绕过反调试。所以,将BLX R7修改成nop( 00 00 A0 E1),找到该指令的偏移地址0x1C58,然后用010Editor修改它。
 
接着,回编译,签名,重新安装。在函数Java_com_yaotong_crackme_MainActivity_securityCheck里循环中第一条指令LDRB R3, [R2]处下断点,attach进程之后,点击“输入密码”按钮,然后F9运行到断点。同样绕过反调试成功。


参考文献:

GoSSIP给出的5题的writeup
AliCrackme_2-frida-简单解法
新手关于ida动态调试so的一些坑总结
2015年AliCrackMe第二题的分析之人肉过反调试
对安卓反调试和校验检测的一些实践与结论


 


看雪ID:直木

https://bbs.pediy.com/user-home-830671.htm

*本文由看雪论坛 直木 原创,转载请注明来自看雪社区





# 往期推荐

1. JSONP和CORS跨域漏洞学习笔记

2. [PwnMonkey]云丁鹿客门锁BLE通信的分析

3. Windows驱动编程之WFP/TDI

4. 记一次MEMZ样本分析

5. GlobeImposter家族的病毒样本分析

6. CVE-2010-2553 堆溢出漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存